﻿// Jinx AJAX Framework
// Copyright (C) 2008 Blake Hegerle
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

function log() {
    console.log.apply(console, arguments);
}

function logException(what, e) {
    console.error("Exception while %s: %s", what, e);
}

/* Actions */

function ActionCallback(transport, controller, method, opts) {
    var action="Action '" + method + "' on '" + controller + "'";
    
    var onerror=opts.onError;
    var onsuccess=opts.onSuccess;

    if(transport.responseJSON != null) {
        var Response=transport.responseJSON;
        
        if(Response.Success) {
            if(onsuccess)
                onsuccess();
        } else {
            if(onerror) {
                onerror();
            } else if(Response.ExceptionSummary != null) {
                alert(action + ' caused server-side exception: ' + Response.ExceptionSummary.Message);
            } else {
                alert(action + ' failed');
            }
        }
    } else {
        if(transport.status != 200) {
            alert(action + ' failed with HTTP status ' + transport.status);
        }
    }
}

function Action(opts) {
    var controller=opts.controller;
    var action=opts.action;

    var Url= 'action/' + controller + '/' + action + '.action';
    
    new Ajax.Request(Url, { 
        method: 'post', 
        postBody: Object.toJSON(opts.argument),
        onFailure: function(transport) { ActionCallback(transport, controller, action, opts); },
        onSuccess: function(transport) { ActionCallback(transport, controller, action, opts); }
    });
}

/* binding */

var RegistrarClass = Class.create({
    initialize: function() {
        this.BindingMap=$H();
    },
    
    bind: function(ID, path) {
        var BindingMap=this.BindingMap;
    
        if(BindingMap.get(path) == undefined)
            BindingMap.set(path, new Array());
         
        var Bindings=BindingMap.get(path);
        
        for(var i=0; i<Bindings.length; i++) {
            if(Bindings[i].ID == ID)
                return;
        }
        
        /* If we got here, then we've got to add a new binding */
        
        var value=null;
        if(Bindings.length > 0) 
            value=Bindings[0].value;
        
        Bindings.push(ID);
    }
});

var Registrar=new RegistrarClass();

function register(ID, path, updateMethod) { /* Use this method when binding during page-load. */    
    Registrar.bind(ID, path, updateMethod);
}

function InitBindings() {
    var Request = {BindingPaths: Registrar.BindingMap.keys()};
    
    Action({controller: 'binding', action: 'bind', argument: Request});
}

function Bind(ID, path) {/* Use this method when binding after page-load. */
    var OldBindingMap=Registrar.BindingMap.get(path);

    Registrar.bind(ID, path);
    
    if(OldBindingMap == undefined) {
        Action({
            controller: 'binding', 
            action: 'bind', 
            argument: {BindingPaths: [path]}, 
            onError: function() { alert('Could not bind ' + path); }
        });
    }
}

/* Polling */

function ProcessUpdates(ReturnValue) {
    if(ReturnValue == null)
        return;
        
    var Updates=ReturnValue['Updates'];
    
    if(Updates == null)
        return;
        
    for(var i=0; i<Updates.length; i++) {
        var Update=Updates[i];
        
        var BindingClients=Registrar.BindingMap.get(Update.Path);
        
        if(BindingClients != undefined) {
            BindingClients.each(function(BindingClient) {
                if(Update.Valid)
                    BindingClient.setValue(Update.Object);
                else
                    BindingClient.setValue(null);
            });
        }
    }
}

function PollingCallback(transport) {
    if(transport.status != 200) {
        alert('Connection to server lost - status ' + transport.status);
    } else {
        var Timeout=10000;
        
        var Response=transport.responseJSON;
        
        if(Response != null) {
            if(Response.Success == false) {
                if(Response.ExceptionSummary != null) {
                    alert('Server-side exception caused by polling: ' + Response.ExceptionSummary.Message);
                } else {
                    alert('Polling failed');
                }
            } else {
                Timeout=100;
                ProcessUpdates(Response.ReturnValue);
            }
        } 
           
        setTimeout('Poll();', Timeout);
    } 
} 

function Poll() {
    new Ajax.Request('polling', { 
        method: 'post', 
        onFailure: PollingCallback,
        onSuccess: PollingCallback
    });
}

function StartJinx() {
    Poll();
}

/*****************************************************************************
* Views                                                                      *
*****************************************************************************/
var Control = Class.create({
    construct: function() {
        this.element=this.createElement();
        this.attach(this.element);
        
        return this.element;
    },

    createElement: function() { return null; },
    
    attach: function(id) {
        this.element=$(id);
    },
    
    createElement: function() {}
});

var HtmlControl = Class.create(Control, {
    initialize: function(name, children, attributes) {
        this.name=name;
        this.children=children;
        this.attributes=$H(attributes);
    },
    
    createElement: function() {
        var Element=document.createElement(this.name);
        
        this.attributes.each(function(a) {
            Element.setAttribute(a.key, a.value);
        });
        
        this.children.each(function(c) {
            Element.appendChild(c.createElement());
        });
        
        return Element;
    }
});

var TextControl = Class.create(Control, {
    initialize: function(text) {
        this.text=text;
    },
    
    createElement: function() {
        return document.createTextNode(this.text);
    }
});

/******************************************************************************
* Controls                                                                    *
******************************************************************************/
var BoundControl = Class.create(Control, {
    initialize: function(path, opts) {
        var tag=opts.tag;
    
        if(tag == undefined)
            log("Tag name cannot be undefined");
    
        var root=opts.pathRoot;
        
        if(root) {
            this.path=root + path;
        } else {
            this.path=path;
        }
        
        this.tag=tag;
    },
    
    attach: function($super, id) {
        $super(id);
        
        var target=this;
        Bind(this, this.path, this.setValue.bind(this));
    },
    
    createElement: function() {
        this.element=document.createElement(this.tag);
        
        this.attach(this.element);
        
        return this.element;
    },
    
    getEventObserver: function() { 
        return function(event) {
            this.sendUpdate();
        }.bindAsEventListener(this);
    },
    
    sendUpdate: function() {
        var Update = {
            BindingPath: this.path,
            Value: this.getValue()
        };
        
        Action({
            controller: 'binding', 
            action: 'update', 
            argument: Update
        });
    },
    
    getValue: function() { return null; },
    
    setValue: function(value) { }
});

var InlineControl = Class.create(BoundControl, {
    setValue: function(value) { this.element.innerHTML=value; }
});

var InputControl = Class.create(BoundControl, {
    attach: function($super, id) {
        $super(id);
        
        this.element.observe('change', this.getEventObserver());
    },
    
    getValue: function() { 
        return this.element.getValue();
    },
    
    setValue: function(value) {
        this.element.setValue(value); 
    }
});

var IntControl = Class.create(InputControl, {
    getValue: function() { 
        return parseInt(this.element.getValue());
    }
});

var FloatControl = Class.create(InputControl, {
    getValue: function() {
        return parseFloat(this.element.getValue());
    }
});

var ActionControl = Class.create(Control, {
    initialize: function($super, action, opts) {
        $super(null, opts);
    
        this.controller=opts.Controller;
        this.param=opts.Param;
        this.action=action;
    },
    
    invoke: function(argument) {
        var args={};
        
        args[this.param] = argument;
    
        Action({
            controller: this.controller, 
            action: this.action, 
            argument: args,
            onError: this.onError,
            onSuccess: this.onSuccess
        });
    }
});

var OnClickControl = Class.create(ActionControl, {
    initialize: function($super, action, opts) {
        $super(action, opts);
        this.children=opts.prototypeFunction();
    },
    
    attach: function($super, id) {
        $super(id);
    
        var element=this.element;
        
        this.children.each(function(c) {
            element.appendChild(c.construct());
        });
    
        var self=this;
        element.observe('click', function() { self.invoke(null); });
    }
});

var CommandControl = Class.create(ActionControl, {
    initialize: function($super, action, opts) {
        $super(action, opts);        
    },
    
    attach: function($super, id) {
        $super(id);
        
        this.element.observe('keydown', this.keyDown.bindAsEventListener(this));
        
        var element=this.element;
        
        this.onSuccess=function() { 
            element.setValue('');
        }
    },
    
    keyDown: function(e) {
        var key=e.keyCode;
        
        if(key == Event.KEY_RETURN)
            this.invoke(this.element.getValue());
    }
});

var ViewTemplate = Class.create(BoundControl, {
    prototypeFunction: function(pathRoot) { return []; },
    
    attach: function($super, id) {
        this.views=this.prototypeFunction(this.path);
        
        $super(id);
    },
    
    setValue: function(value) {
        this.element.childElements().each(function(e) {
            e.remove();
        });
        
        var container=this.element;

        if(value && this.views) {
            this.views.each(function(v) {
                container.appendChild(v.construct());
            });
        }
    }
});

var SequenceControl = Class.create(BoundControl, {
    initialize: function($super, path, opts) {
        $super(path + '.Count', opts);
      
        this.Count=0;
        this.seqPathRoot=path;
        this.prototypeFunction=opts.prototypeFunction;
    },
    
    setValue: function(value) {
        if(!value) value=0;
    
        if(value > this.Count) {
            for(var i=this.Count; i<value; i++) {
                var path=this.seqPathRoot + '[' + i + ']';
                var parent=this.element;
            
                this.prototypeFunction(path).each(function(v) {
                    parent.appendChild(v.construct());
                });
            }
        } else if(value < this.Count) {
            var Children=this.element.childElements();
            
            for(var i=value; i<this.Count; i++) {
                Children[i].remove();
            }
        }
        
        this.Count=value;
    }
});

/* Pseudo-controls */

var ClassToggleControl = Class.create(BoundControl, {
    initialize: function($super, path, opts) {
        $super(path, opts);
        
        this.className=opts.ClassName;
    },
    
    setValue: function(value) {
        if(Boolean(value)) 
            this.element.up().addClassName(this.className);
        else
            this.element.up().removeClassName(this.className);
    }
});
